﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Metadata.Edm;
using System.Data.Objects;
using System.Linq;
using NLog;

namespace Jet.Dal.EF.Implementation
{
	/// <summary>
	/// Implementation of a IRepository for EntityFramework 4.X-5.0
	/// </summary>
	public class EFRepository<TEntity> : IEFRepository<TEntity>
		where TEntity : class
	{
		private readonly DbContext _context;
		private readonly DbSet<TEntity> _set;
		private readonly Logger _logger = LogManager.GetCurrentClassLogger();
		private readonly string _entitySetName;
		private readonly ObjectContext _objectContext;
		/// <summary>
		/// .ctor
		/// <exception cref="ArgumentNullException">If context is null</exception>
		/// <exception cref="InvalidOperationException">If cannot get entity container</exception>
		/// </summary>
		public EFRepository(DbContext context)
		{
			if (context == null)
			{
				throw new ArgumentNullException("context");
			}
			_context = context;
			_set = _context.Set<TEntity>();
			_objectContext = ((IObjectContextAdapter)_context).ObjectContext;
			var entityType = typeof(TEntity);
			var container = _objectContext.MetadataWorkspace.GetEntityContainer(_objectContext.DefaultContainerName, 
				DataSpace.CSpace);
			string entitySetName;
			do
			{
				entitySetName = (from meta in container.BaseEntitySets
				                 where meta.ElementType.Name == entityType.Name
				                 select meta.Name).FirstOrDefault();
				entityType = entityType.BaseType??typeof(object);
			} while ((entitySetName == null) && (entityType != typeof (object)));

			if (entitySetName==null)
			{
				throw new InvalidOperationException("Cannot find container for given entity type");
			}
			_entitySetName = String.Format("{0}.{1}", container.Name, entitySetName);

			_logger.Debug("Repository for type {0} created", typeof(TEntity));
		}

		/// <summary>
		/// <see cref="IEFRepository{TEntity}"/>
		/// </summary>
		public IQueryable<TEntity> GetQuery()
		{
			DbQuery<TEntity> result = _set;
			return result;
		}

		/// <summary>
		/// <see cref="IEFRepository{TEntity}"/>
		/// <exception cref="ArgumentNullException">If navigations is null or some member of the collection is null or empty</exception>
		/// </summary>
		public IQueryable<TEntity> GetQuery(IEnumerable<string> navigations)
		{
			if (navigations== null)
			{
				throw new ArgumentNullException("navigations");
			}
			DbQuery<TEntity> result = _set;
			foreach(var navigation in navigations)
			{
				if (String.IsNullOrEmpty(navigation))
				{
					throw new ArgumentNullException("navigations");
				}
				result = result.Include(navigation);
			}
			return result;
		}

		/// <summary>
		/// <see cref="IEFRepository{TEntity}"/>
		/// </summary>
		public EntityState GetState(TEntity entity)
		{
			if(entity==null)
			{
				throw new ArgumentNullException("entity");
			}
			var internalState = _context.Entry(entity).State;
			switch(internalState)
			{
				case System.Data.EntityState.Detached:
					return EntityState.Detached;
				case System.Data.EntityState.Unchanged:
					return EntityState.Unchanged;
				case System.Data.EntityState.Added:
					return EntityState.Added;
				case System.Data.EntityState.Deleted:
					return EntityState.Deleted;
				case System.Data.EntityState.Modified:
					return EntityState.Modified;
				default:
					throw new ArgumentOutOfRangeException("entity");
			}
		}

		/// <summary>
		/// <see cref="IEFRepository{TEntity}"/>
		/// <exception cref="ArgumentNullException">If source is null</exception>
		/// <exception cref="InvalidOperationException">When the entity key cannot be constructed 
		/// successfully based on the supplied parameters.</exception>
		/// </summary>
		public EntityKey CreateKey(TEntity source)
		{
			if (source==null)
			{
				throw new ArgumentNullException("source");
			}
			var key = _objectContext.CreateEntityKey(_entitySetName, source);
			return key;
		}

		/// <summary>
		/// <see cref="IEFRepository{TEntity}"/>
		/// <exception cref="ArgumentNullException">If key is null</exception>
		/// <exception cref="ArgumentException">If key has wrong type</exception>
		/// </summary>
		public bool TryGetAttachedObjectByKey(EntityKey key, out TEntity attachedObject)
		{
			if (key == null)
			{
				throw new ArgumentNullException("key");
			}
			object attachedInternal;
			var result = _objectContext.TryGetObjectByKey(key, out attachedInternal);
			attachedObject = (TEntity) attachedInternal;
			return result;
		}

		/// <summary>
		/// <see cref="IEFRepository{TEntity}"/>
		/// <exception cref="ArgumentException">If key is not null and not EntityKey</exception>
		/// </summary>
		public TEntity Create(EntityKey key = null)
		{			
			var result = _set.Create<TEntity>();
			if (key==null)
			{
				return result;
			}
			foreach (var pair in key.EntityKeyValues)
			{
				var property = result.GetType().GetProperty(pair.Key);
				property.SetValue(result, pair.Value, null);
			}
			return result;
		}

		/// <summary>
		/// <see cref="IEFRepository{TEntity}"/>
		/// </summary>
		public void Attach(TEntity entity)
		{
			if (entity == null)
			{
				throw new ArgumentNullException("entity");
			}
			_set.Attach(entity);
		}

		/// <summary>
		/// <see cref="IEFRepository{TEntity}"/>
		/// </summary>
		public void Insert(TEntity entity)
		{
			if (entity == null)
			{
				throw new ArgumentNullException("entity");
			}
			_set.Add(entity);
		}

		/// <summary>
		/// <see cref="IEFRepository{TEntity}"/>
		/// </summary>
		public void Update(TEntity entity)
		{
			if (entity == null)
			{
				throw new ArgumentNullException("entity");
			}
			_context.Entry(entity).State = System.Data.EntityState.Modified;
		}

		/// <summary>
		/// <see cref="IEFRepository{TEntity}"/>
		/// </summary>
		public void Delete(TEntity entity)
		{
			if (entity == null)
			{
				throw new ArgumentNullException("entity");
			}
			_set.Remove(entity);
		}

		/// <summary>
		/// <see cref="IEFRepository{TEntity}"/>
		/// </summary>
		public void Detach(TEntity entity)
		{
			if (entity == null)
			{
				throw new ArgumentNullException("entity");
			}
			_context.Entry(entity).State = System.Data.EntityState.Detached;
		}
	}
}
